package three_star_craft.uap;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;

import org.grlea.log.SimpleLogger;

import three_star_craft.EventData;
import three_star_craft.uap.event.EventUapConnectionCfm;
import three_star_craft.uap.event.EventUapConnectionRej;
import three_star_craft.uap.event.EventUapConnectionReleaseCfm;
import three_star_craft.uap.event.EventUapMediaBearerSetupCfm;
import three_star_craft.uap.state.UapStateMap;
import ProtocolsCommon.Common.IpAddress;
import UserApplicationPart.UAP;
import UserApplicationPart.UAP.UapConnectionConfirm;
import UserApplicationPart.UAP.UapConnectionReject;
import UserApplicationPart.UAP.UapConnectionReleaseCfm;
import UserApplicationPart.UAP.UapConnectionReleaseReq;
import UserApplicationPart.UAP.UapMediaBearerSetupConfirm;
import UserApplicationPart.UAP.UapMediaBearerSetupRequest;
import UserApplicationPart.UAP.UapPdu;
import UserApplicationPart.UAP.UapPduNumber;

import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;


/**
 * Proxy for communication between UapGenerators and serving gateway
 * @author w.antosiewic
 *
 */
public class UapSgwProxy
{
    private static final SimpleLogger logger = new SimpleLogger(UapSgwProxy.class);

    /**
     * IP address of Serving Gateway
     */
    private InetAddress _sgwAddress;
    /**
     * Remote port in Serving Gateway for UAP protocol 
     */
    private int _sgwPort;

    /**
     * Socket for User interface (UAP protocol)
     */
    private Socket _uapSocket;
    /**
     * Thread for listening on uap socket
     */
    private UapSocketThread _uapSocketThread;
    /**
     * Flag indicating whether uapSocketThread has been started and
     * is running
     */
    private boolean _uapSocketRunning = false;

    /**
     * Guard timer for establishment of socket towards SGW.
     * Value in milliseconds.
     */
    private static int CONN_REQ_TIMEOUT = 5000;

    /**
     * Singleton instance
     */
    private static UapSgwProxy _instance;

    /**
     * Private constructor called during proxy initialization
     * @param sgwAddress IP address of Serving gateway
     * @param port port of serving gateway U interface
     */
    private UapSgwProxy(InetAddress sgwAddress, int port)
    {
        _sgwAddress = sgwAddress;
        _sgwPort = port;
        initiateSocket();
    }

    /**
     * Returns singleton instance of the proxy
     * @return instance of this proxy or <code>null</code> if proxy has
     * not been initialized
     * 
     * {@linkplain UapSgwProxy#init(InetAddress, int)}
     */
    public static UapSgwProxy instance()
    {
        if(_instance == null)
        {
            logger.error("UapSgwProxy has not been initialized");
        }
        return _instance;
    }

    /**
     * Initializes this proxy with specified IP address and port of
     * Serving Gateway. This method must be called before first use
     * of the proxy.
     * 
     * @param sgwAddress IP address of Serving gateway
     * @param sgwPort port of serving gateway U interface
     */
    public static void init(InetAddress sgwAddress, int sgwPort)
    {
        logger.debugObject("Creating UapSgwProxy with address "+sgwAddress+", port=", sgwPort);
        _instance = new UapSgwProxy(sgwAddress, sgwPort);
    }

    public InetAddress getSgwIp()
    {
        return _sgwAddress;
    }
    
    private boolean initiateSocket()
    {
        try
        {
            if (_uapSocketThread != null)
            {
                logger.verbose("UapSocket thread already initiated, returning");
                return true;
            }
            logger.debug("Creating socket towards SGW, ip="+_sgwAddress+", port="+_sgwPort);

            // create socket for communication with SGW
            _uapSocket = new Socket(_sgwAddress, _sgwPort);
            // bind socket with local address and port - argument null
            // means that port and address will be selected automatically
            if(_uapSocket.isBound() == false)
            {
                logger.verboseObject("Binding socket:", _uapSocket);
                _uapSocket.bind(null);
            }
            logger.debugObject("Socket bound:", _uapSocket);
            if (_uapSocket.isBound() && !_uapSocket.isConnected())
            {
                logger.debugObject("Socket bound, connecting... :", _uapSocket);
                _uapSocket.connect(new InetSocketAddress(_sgwAddress, _sgwPort), CONN_REQ_TIMEOUT);
            }

            _uapSocketThread = new UapSocketThread();


            if(_uapSocketRunning == false)
            {
                logger.debug("Starting UapSocketThread");
                _uapSocketRunning = true;
                _uapSocketThread.start();
            }
        }
        catch(SocketTimeoutException e)
        {
            logger.errorException(e);
            return false;
        }
        catch (IOException e)
        {
            logger.errorException(e);
            return false;
        }
        return true;
    }

    /**
     * Sends UAP ConnectionRequest message for the specified UAP generator
     * @param gen for which the connection shall be established
     * @return <code>true</code> if message was send, <code>false</code> otherwise
     */
    public boolean sendUapConnectionReq(UapGenerator gen)
    {
        if(!initiateSocket())
        {
            logger.debugObject("Socket initiation failed - unable to send UapConnectionReq", gen.getDebugInfo());
            return false;
        }
        logger.debug("Socket initiation succeded, creating UapConnectionReq message");
        // builder for UapConnectionRequest message
        UAP.UapConnectionRequest.Builder conReqBuilder = 
                UAP.UapConnectionRequest.newBuilder();

        IpAddress.Builder ipAddrBuilder = IpAddress.newBuilder();
        ipAddrBuilder.setIpBytes(ByteString.copyFrom(_uapSocket.getLocalAddress().getAddress()));

        conReqBuilder.setSourceIp(ipAddrBuilder);
        conReqBuilder.setRandomAccessPort(_uapSocket.getLocalPort());

        UAP.ClientCapabilities.Builder capBuilder = UAP.ClientCapabilities.newBuilder();
        capBuilder.setVideo(gen.getVideoCapabilities());
        capBuilder.setAudio(gen.getAudioCapabilities());

        conReqBuilder.setCapabilities(capBuilder);

        // build the message from builder
        if(!conReqBuilder.isInitialized())
        {
            logger.error("Unable to create UapConnectionReq - not all required fields are set");
            return false;
        }
        UAP.UapConnectionRequest msg = conReqBuilder.build();

        if(!sendUapPdu(gen.getConnectionId(), 
                UapPduNumber.UAP_PDU_CONNECTION_REQ,
                msg.toByteString(),
                _uapSocket))
        {
            logger.warn("Sending UapConnectionReq failed");
            return false;
        }

        gen.startTWaitForConnCfm();
        gen.changeState(UapStateMap.UAP_CONNECT_PEND);

        return true;
    }

    private boolean sendUapPdu(int connectionId, 
                               UapPduNumber pduNumber,
                               ByteString bytes, 
                               Socket s)
    {
        try
        {
            UapPdu uapPdu = UapPdu.newBuilder()
                    .setPduNumber(pduNumber)
                    .setPdu(bytes)
                    .setConnectionId(connectionId)
                    .build();

            uapPdu.writeTo(s.getOutputStream());
            s.getOutputStream().flush();
            logger.verboseObject("UAP_BUS_SEND: "+pduNumber+",size="+
                    uapPdu.getSerializedSize(), uapPdu.toByteArray());
        } catch (IOException e)
        {
            logger.errorException(e);
            return false;
        }

        return true;
    }

    public boolean sendMediaBearerRequest(
            UapGenerator uapGenerator,
            int playableItemId)
    {
        UapMediaBearerSetupRequest.Builder mediaBearerReqBuilder =
                UapMediaBearerSetupRequest.newBuilder();
        
        mediaBearerReqBuilder.setMediaItemId(playableItemId);
        mediaBearerReqBuilder.setClientMediaPort(uapGenerator.getMediaPort());
        
        if(!mediaBearerReqBuilder.isInitialized())
        {
            logger.error("Unable to create UapMediaBearerReq, errorStr"+
                    mediaBearerReqBuilder.getInitializationErrorString());
            return false;
        }
        UapMediaBearerSetupRequest mediaBearerReq = mediaBearerReqBuilder.build();
        
        if(!sendUapPdu(uapGenerator.getConnectionId(),
                       UapPduNumber.UAP_PDU_MEDIA_BEARER_SETUP_REQ,
                       mediaBearerReq.toByteString(),
                       _uapSocket))
        {
            logger.warn("Sending of MediaBearerReq failed");
            return false;
        }
        
        return true;
    }

    public void sendUapConnectionRelease(UapGenerator uapGenerator)
    {
        UapConnectionReleaseReq.Builder releaseReqBuilder = 
                UapConnectionReleaseReq.newBuilder();

        if(!releaseReqBuilder.isInitialized())
        {
            logger.warn("Unable to send UapConnectionReleaseReq, errorStr:"+
                    releaseReqBuilder.getInitializationErrorString());
            return;
        }

        if(!sendUapPdu(uapGenerator.getConnectionId(),
                UapPduNumber.UAP_PDU_CONNECTION_RELEASE_REQ,
                releaseReqBuilder.build().toByteString(),
                _uapSocket))
        {
            logger.warn("Sending UapConnectionReleaseReq failed");
        }
    }

    /**
     * Thread for handling incoming data on the UAP socket
     * @author w.antosiewic
     *
     */
    class UapSocketThread extends Thread
    {

        private InputStream socketInputStream;
        // 5kb buffer for reading from the stream
        private byte[] buf = new byte[5 * 1024];

        public UapSocketThread()
        {
            try
            {
                socketInputStream = _uapSocket.getInputStream();
            } catch (IOException e)
            {
                logger.errorException(e);
            }
        }

        public void run()
        {
            while(_uapSocketRunning)
            {
                try
                {
                    // read data from stream into provided buffer - 
                    // blocking operation until some bytes are available
                    int numBytes = socketInputStream.read(buf);

                    // create ByteString object by reading from 'buf' the exact number
                    // of received bytes
                    ByteString bs = ByteString.copyFrom(buf, 0, numBytes);
                    logger.verboseObject("UapSocketThread - received data, size="+
                            numBytes+", bytes:", bs.toByteArray());

                    UapPdu uapPdu = UapPdu.parseFrom(bs);
                    if(!uapPdu.isInitialized())
                    {
                        logger.warn("Received invalid UapPdu from bytes:"+buf);
                        logger.verboseObject("Invalid PDU, errorStr:"+
                                uapPdu.getInitializationErrorString()+", pdu:", buf); 
                    }
                    else
                    {
                        logger.debugObject("Received valid UapPdu on for connId=",
                                Integer.toHexString(uapPdu.getConnectionId()));

                        handleUapPdu(uapPdu);
                    }

                } catch (IOException e)
                {
                    logger.errorException(e);
                }


                try
                {
                    Thread.sleep(100);
                } catch (InterruptedException e) {    
                    logger.errorException(e);
                }
            }// while
        }// run

        private void handleUapPdu(UapPdu pdu)
        {
            logger.debugObject("handling UapPdu with pduNum="+pdu.getPduNumber()+
                    ", connId=",Integer.toHexString(pdu.getConnectionId()));
            EventData event = null;
            UapGenerator generator = UapGenerator.findGenerator(pdu.getConnectionId());

            if(generator == null)
            {
                logger.warn("Couldn't find generator for connId=0x"+
                        Integer.toHexString(pdu.getConnectionId()));
                return;
            }

            switch(pdu.getPduNumber()){
            case UAP_PDU_CONNECTION_CFM:
            {
                try
                {
                    UapConnectionConfirm connCfm = UapConnectionConfirm.parseFrom(pdu.getPdu());
                    if(!connCfm.isInitialized())
                    {
                        logger.warn("Invalid UapConnectionCfm received, errorStr:"+
                                connCfm.getInitializationErrorString());
                        break;
                    }

                    event = new EventUapConnectionCfm(connCfm);

                } catch (InvalidProtocolBufferException e)
                {
                    logger.errorException(e);
                }
            }
            break;
            
            case UAP_PDU_CONNECTION_REJ:
            {
                try
                {
                    UapConnectionReject connRej = UapConnectionReject.parseFrom(pdu.getPdu());
                    if(!connRej.isInitialized())
                    {
                        logger.warn("Invalid UapConnectionRej received, errorStr:"+
                                connRej.getInitializationErrorString());
                        break;
                    }
                    event = new EventUapConnectionRej(connRej);

                } catch (InvalidProtocolBufferException e)
                {
                    logger.errorException(e);
                }
            }
            break;
            
            case UAP_PDU_CONNECTION_RELEASE_CFM:
            {
                try
                {
                    UapConnectionReleaseCfm connRelCfm =
                            UapConnectionReleaseCfm.parseFrom(pdu.getPdu());
                    if(!connRelCfm.isInitialized())
                    {
                        logger.warn("Invalid UapConnectionReleaseCfm received, errorStr:"+
                                connRelCfm.getInitializationErrorString());
                        break;
                    }
                    event = new EventUapConnectionReleaseCfm(connRelCfm);

                } catch (InvalidProtocolBufferException e)
                {
                    logger.errorException(e);
                }
            }
            break;
            
            case UAP_PDU_MEDIA_BEARER_SETUP_CFM:
            {
                try
                {
                    UapMediaBearerSetupConfirm mbSetupCfm = 
                            UapMediaBearerSetupConfirm.parseFrom(pdu.getPdu());
                    if(!mbSetupCfm.isInitialized())
                    {
                        logger.warn("Invalid UapMediaBearerSetupConfirm received, errorStr:"+
                                mbSetupCfm.getInitializationErrorString());
                        break;
                    }
                    event = new EventUapMediaBearerSetupCfm(mbSetupCfm);
                            
                } catch (InvalidProtocolBufferException e)
                {
                    logger.errorException(e);
                }
            }
            break;
            
            default:
                logger.warn("Unexpected UapPdu = "+pdu.getPduNumber());
            }

            if(event != null)
            {
                generator.handleEvent(event);
            }
        }
    }// UapSocketThread
}
